﻿using System;
using System.Collections.Generic;

using FirstFloor.P2P.Reversi.Messages;

namespace FirstFloor.P2P.Reversi
{
    /// <summary>
    /// Implements the Reversi game.
    /// </summary>
    public class ReversiGame
        : TurnBasedGame
    {
        /// <summary>
        /// Occurs when an info message should be displayed.
        /// </summary>
        public event EventHandler<InfoMessageEventArgs> InfoMessage;

        private Board board;
        private Dictionary<PieceColor, List<Location>> validMoves;
        private bool hintsEnabled = true;
        private PieceColor myColor;
        private PieceColor restartInitiator = PieceColor.None;

        /// <summary>
        /// Initializes a new instance of the <see cref="ReversiGame"/> class.
        /// </summary>
        /// <param name="board">The board.</param>
        public ReversiGame(Board board)
        {
            if (board == null) {
                throw new ArgumentNullException("board");
            }

            this.board = board;
            this.board.PieceSelected += board_PieceSelected;

            this.validMoves = new Dictionary<PieceColor, List<Location>>();

            P2PApplication.Current.DataReceived += OnDataReceived;

            InitializeGame();

            Connect();
        }

        /// <summary>
        /// Gets or sets a value indicating whether hints are enabled.
        /// </summary>
        /// <value><c>true</c> if hints are enabled; otherwise, <c>false</c>.</value>
        public bool HintsEnabled
        {
            get { return this.hintsEnabled; }
            set
            {
                if (this.hintsEnabled != value) {
                    this.hintsEnabled = value;

                    if (this.GameState == TurnBasedGameState.MyTurn) {
                        if (value) {
                            MarkValidMoves(this.myColor);
                        }
                        else {
                            this.board.ClearMarks();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Restarts this instance.
        /// </summary>
        public void Restart()
        {
            if (this.GameState == TurnBasedGameState.GameOver) {
                bool startMyTurn = GameStartedWithMyTurn;
                if (GameResult == TurnBasedGameResult.Won) {
                    startMyTurn = true;
                }
                else if (GameResult == TurnBasedGameResult.Lost) {
                    startMyTurn = false;
                }
                StartGame(startMyTurn);
            }
            else if (GameState == TurnBasedGameState.MyTurn || GameState == TurnBasedGameState.RemoteTurn) {
                SendMessage(new RestartMessage());

                if (this.restartInitiator == GetOppositeColor(this.myColor)) {
                    StartGame(GameStartedWithMyTurn);
                }
                else {
                    OnInfoMessage("A restart request has been sent to your opponent. Please wait for confirmation.");
                    this.restartInitiator = this.myColor;
                }
            }
        }

        /// <summary>
        /// Occurs when the game state has changed.
        /// </summary>
        /// <param name="gameState">State of the game.</param>
        /// <param name="closeReason">The close reason.</param>
        protected override void OnGameStateChanged(TurnBasedGameState gameState, P2PApplicationCloseReason closeReason)
        {
            if (gameState == TurnBasedGameState.Loaded) {
                this.myColor = this.Users.Me.IsInviter ? PieceColor.White : PieceColor.Red;

                StartGame(this.Users.Me.IsInviter);
            }
            else if (gameState == TurnBasedGameState.StartingGame) {
                InitializeGame();
            }
            else if (gameState == TurnBasedGameState.MyTurn) {
                this.restartInitiator = PieceColor.None;
                SetValidMovesSelectable(this.myColor);

                if (this.hintsEnabled) {
                    MarkValidMoves(this.myColor);
                }

                OnInfoMessage("It's your turn");
            }
            else if (gameState == TurnBasedGameState.RemoteTurn) {
                this.restartInitiator = PieceColor.None;
                OnInfoMessage("Please wait for your opponent to make a move");
            }
            else if (gameState == TurnBasedGameState.GameOver) {
                string message = "it's a draw.";
                if (GameResult == TurnBasedGameResult.Won) {
                    message = "you have won!";
                }
                else if (GameResult == TurnBasedGameResult.Lost) {
                    message = "you have lost...";
                }

                OnInfoMessage(string.Format("Game over, {0} Press 'Restart' for a new game.", message));
            }
            else if (gameState == TurnBasedGameState.Closed) {
                if (closeReason == P2PApplicationCloseReason.ClosedByLocalApplication) {
                    OnInfoMessage("You have closed the application");
                }
                else if (closeReason == P2PApplicationCloseReason.ClosedByRemoteApplication) {
                    OnInfoMessage("Remote has closed the application");
                }
                else if (closeReason == P2PApplicationCloseReason.ConnectTimeout) {
                    OnInfoMessage("Timeout while trying to connect to remote application");
                }
                this.board.ClearMarksAndSelectables();
            }
        }

        private void OnDataReceived(object sender, P2PValueEventArgs e)
        {
            Message message = Message.CreateMessage(e.Value);

            if (message is MovePieceMessage) {
                MovePieceMessage move = (MovePieceMessage)message;

                MovePiece(move.X, move.Y, GetOppositeColor(this.myColor));
            }
            else if (message is RestartMessage) {
                if (this.restartInitiator == this.myColor) {
                    StartGame(GameStartedWithMyTurn);
                }
                else {
                    OnInfoMessage("Opponent requests a game restart. Press 'Restart' to confirm.");
                    this.restartInitiator = GetOppositeColor(this.myColor);
                }
            }
        }

        private void OnInfoMessage(string message)
        {
            if (InfoMessage != null) {
                InfoMessage(this, new InfoMessageEventArgs(message));
            }
        }

        private void board_PieceSelected(object sender, LocationEventArgs e)
        {
            if (this.GameState == TurnBasedGameState.MyTurn) {
                int x = e.Location.X;
                int y = e.Location.Y;

                if (IsValidMove(x, y, this.myColor)) {
                    MovePiece(x, y, this.myColor);
                    SendMessage(new MovePieceMessage(x, y));
                }
            }
        }

        private void SendMessage(Message message)
        {
            P2PApplication.Current.SendData(message.ToString());
        }

        private void InitializeGame()
        {
            this.board.Clear();
            this.board[3, 3] = PieceColor.Red;
            this.board[3, 4] = PieceColor.White;
            this.board[4, 3] = PieceColor.White;
            this.board[4, 4] = PieceColor.Red;

            CalculateValidMoves();
        }

        private List<Location> GetAdjacentLocationsHavingColor(int x, int y, PieceColor color)
        {
            List<Location> locations = new List<Location>();

            for (int i = Math.Max(0, x - 1); i < Math.Min(Board.BoardLength, x + 2); i++) {
                for (int j = Math.Max(0, y - 1); j < Math.Min(Board.BoardLength, y + 2); j++) {
                    if (!(x == i && y == j) && this.board[i, j] == color) {
                        locations.Add(new Location(i, j));
                    }
                }
            }
            return locations;
        }

        private bool CalculateValidMove(int x, int y, PieceColor color)
        {
            PieceColor oppositeColor = GetOppositeColor(color);
            List<Location> locations = GetAdjacentLocationsHavingColor(x, y, oppositeColor);

            foreach (Location location in locations) {
                int deltaX = location.X - x;
                int deltaY = location.Y - y;

                int nextX = x + deltaX;
                int nextY = y + deltaY;

                while (nextX > -1 && nextX < Board.BoardLength && nextY > -1 && nextY < Board.BoardLength) {
                    PieceColor locationColor = this.board[nextX, nextY];

                    if (locationColor == PieceColor.None) {
                        break;
                    }
                    else if (locationColor == oppositeColor) {
                        nextX += deltaX;
                        nextY += deltaY;
                    }
                    else {
                        return true;
                    }
                }
            }

            return false;
        }

        private void CalculateValidMoves()
        {
            this.validMoves.Clear();

            PieceColor[] colors = new PieceColor[] { PieceColor.Red, PieceColor.White };
            foreach (PieceColor color in colors) {
                this.validMoves.Add(color, new List<Location>());
            }

            for (int x = 0; x < Board.BoardLength; x++) {
                for (int y = 0; y < Board.BoardLength; y++) {
                    if (this.board[x, y] == PieceColor.None) {
                        foreach (PieceColor color in colors) {
                            if (CalculateValidMove(x, y, color)) {
                                this.validMoves[color].Add(new Location(x, y));
                            }
                        }
                    }
                }
            }
        }

        private PieceColor GetOppositeColor(PieceColor color)
        {
            if (color == PieceColor.Red) {
                return PieceColor.White;
            }
            else if (color == PieceColor.White) {
                return PieceColor.Red;
            }
            else {
                throw new ArgumentOutOfRangeException("color");
            }
        }

        private void MarkValidMoves(PieceColor color)
        {
            List<Location> validMoves = this.validMoves[color];

            this.board.ClearMarks();
            foreach (Location validMove in validMoves) {
                this.board.MarkPiece(validMove.X, validMove.Y, true);
            }
        }

        private void SetValidMovesSelectable(PieceColor color)
        {
            List<Location> validMoves = this.validMoves[color];

            this.board.ClearSelectables();
            foreach (Location validMove in validMoves) {
                this.board.SetSelectable(validMove.X, validMove.Y, true);
            }
        }

        private bool IsValidMove(int x, int y, PieceColor color)
        {
            List<Location> moves = this.validMoves[color];
            foreach (Location move in moves) {
                if (move.X == x && move.Y == y) {
                    return true;
                }
            }
            return false;
        }

        private bool IsGameOver()
        {
            return this.validMoves[PieceColor.Red].Count == 0 && this.validMoves[PieceColor.White].Count == 0;
        }

        private bool CanMove(PieceColor color)
        {
            return this.validMoves[color].Count > 0;
        }

        private void MovePiece(int x, int y, PieceColor color)
        {
            if (IsValidMove(x, y, color)) {
                this.board.ClearMarksAndSelectables();

                PieceColor oppositeColor = GetOppositeColor(color);
                List<Location> locations = GetAdjacentLocationsHavingColor(x, y, oppositeColor);

                foreach (Location location in locations) {
                    int deltaX = location.X - x;
                    int deltaY = location.Y - y;
                    List<Location> newLocations = new List<Location>();

                    int nextX = x + deltaX;
                    int nextY = y + deltaY;

                    while (nextX > -1 && nextX < Board.BoardLength && nextY > -1 && nextY < Board.BoardLength) {
                        PieceColor locationColor = this.board[nextX, nextY];

                        if (locationColor == PieceColor.None) {
                            break;
                        }
                        else if (locationColor == oppositeColor) {
                            newLocations.Add(new Location(nextX, nextY));
                            nextX += deltaX;
                            nextY += deltaY;
                        }
                        else {
                            foreach (Location newLocation in newLocations) {
                                this.board[newLocation.X, newLocation.Y] = color;
                            }
                            break;
                        }
                    }
                }

                this.board[x, y] = color;

                CalculateValidMoves();

                // determine is game is over
                if (IsGameOver()) {
                    int myPieces = board.GetPieceCount(this.myColor);
                    int remotePieces = board.GetPieceCount(GetOppositeColor(this.myColor));

                    TurnBasedGameResult result = TurnBasedGameResult.Draw;
                    if (myPieces > remotePieces) {
                        result = TurnBasedGameResult.Won;
                    }
                    else if (myPieces < remotePieces) {
                        result = TurnBasedGameResult.Lost;
                    }

                    StopGame(result);
                }
                else if (CanMove(GetOppositeColor(color))) {
                    SwitchTurn();
                }
                else {
                    // keep turn, opposite can't move
                    OnGameStateChanged(GameState, P2PApplicationCloseReason.None);
                }
            }
        }
    }
}
